<template>
  <div class="index-chart">
    <main ref="main"></main>
    <section ref="tooltip" v-show="visible">
      <div class="topic" v-text="tooltip.topic"></div>
      <div class="label1" v-text="tooltip.xlabel"></div>
      <div class="label1" v-text="tooltip.ylabel"></div>
    </section>
  </div>
</template>
<script lang="ts">
import * as d3 from "d3";
import { Vue, Component, Ref } from "vue-property-decorator";
import Icon from "../TriangleChart/Icon";
type D3Selection = d3.Selection<any, any, any, any>;

@Component({
  name: "IndexChart",
  components: {}
})
export default class IndexChart extends Vue {
  private svg!: D3Selection;
  private zoom!: D3Selection;

  private legends!: D3Selection;
  private defs!: D3Selection;

  private config!: IndexChartConfig;

  private w!: number;
  private h!: number;

  private yLabelWidth: number = 30;
  private xLabelHeight: number = 30;

  private xscale!: d3.ScaleLinear<any, any, any>;
  private yscale!: d3.ScaleLinear<any, any, any>;

  private startX!: number;
  private startY!: number;
  private endX!: number;
  private endY!: number;

  private visible: boolean = false;

  @Ref("tooltip")
  private $ToolTip!: HTMLDivElement;

  private tooltip = {
    topic: "",
    xlabel: "",
    ylabel: ""
  };

  @Ref("main")
  private $main!: HTMLDivElement;

  public setOptions(config: IndexChartConfig) {
    this.$main.innerHTML = "";

    this.config = config;

    if (!this.config.padding) this.config.padding = 20;

    this.config.series.forEach((d, i) => {
      d.label = d.label || "分组" + (i + 1);
      d.points.forEach((p) => {
        p.size = p.size || d.icon?.size || Icon.defaultIconSize;
      });
    });

    const rect = this.$main.getBoundingClientRect();
    this.w = rect.width;
    this.h = rect.height;

    this.startX = this.config.padding + this.yLabelWidth;
    this.startY = this.config.padding;
    this.endX = this.w - this.config.padding - 40;
    this.endY = this.h - this.config.padding - this.xLabelHeight;

    this.initSvg();
    this.initStyle();
    this.initIconRefs();
    this.initAxises();
    this.addGuideLines();
    this.appendPoints();
    this.appendLegends();
    this.bindEvents();
  }

  private initSvg() {
    this.svg = d3
      .select(this.$main)
      .append("svg")
      .attr("width", this.w)
      .attr("height", this.h);

    this.defs = this.svg.append("defs");
    this.zoom = this.svg.append("g").classed("zoom-ctrl", true);
    this.legends = this.svg.append("g").classed("legends-ctrl", true);
  }

  private initStyle() {
    this.defs.append("style").text(`
      .mtxt{
        text-anchor: middle;
        dominant-baseline: middle;
      }
      .x-axis text{fill: ${this.config.axises?.x?.color || "#000"}}
      .x-axis path, .x-axis line{
        stroke: ${this.config.axises?.x?.color || "#000"}
      }
      .y-axis text{fill: ${this.config.axises?.y?.color || "#000"}}
      .y-axis path, .y-axis line{
        stroke: ${this.config.axises?.y?.color || "#000"}
      }
      .point{
        cursor: pointer;
      }
      .point:hover{
        opacity: 0.7;
      }
    `);
  }

  private initIconRefs() {
    this.config.series.forEach((d) => {
      d.id = Icon.createIconMapping(d.icon || {}, this.defs);
    });
  }

  private initAxises() {
    const max = { x: 0, y: 0 };

    this.config.series.forEach((sery) => {
      const x = d3.max(sery.points, (d) => d.x) as number;
      const y = d3.max(sery.points, (d) => d.y) as number;
      if (x > max.x) {
        max.x = x;
      }
      if (y > max.y) {
        max.y = y;
      }
    });

    this.xscale = d3
      .scaleLinear()
      .domain([0, max.x])
      .range([this.startX, this.endX])
      .nice();

    this.yscale = d3
      .scaleLinear()
      .domain([0, max.y])
      .range([this.endY, this.startY])
      .nice();

    const axies = this.zoom.append("g").classed("axies", true);
    axies
      .append("g")
      .classed("x-axis", true)
      .attr("transform", `translate(0, ${this.endY})`)
      .call(d3.axisBottom(this.xscale));

    axies
      .append("g")
      .classed("y-axis", true)
      .attr("transform", `translate(${this.startX}, 0)`)
      .call(d3.axisLeft(this.yscale));

    const labels = axies.append("g").classed("labels", true);
    labels
      .append("text")
      .classed("xlabel", true)
      .classed("mtxt", true)
      .text(this.config.axises?.x?.label || "")
      .attr("x", (this.endX + this.startX) / 2)
      .attr("y", this.endY + 40)
      .style("font-size", this.config.axises?.x?.fontSize || 16)
      .style("fill", this.config.axises?.x?.color || "#000")
      .style("font-weight", this.config.axises?.y?.fontWeight || "bold");
    labels
      .append("text")
      .classed("ylabel", true)
      .classed("mtxt", true)
      .text(this.config.axises?.y?.label || "")
      .attr("x", this.startX - 40)
      .attr("y", (this.startY + this.endY) / 2)
      .style("writing-mode", "tb")
      .style("font-size", this.config.axises?.y?.fontSize || 16)
      .style("fill", this.config.axises?.y?.color || "#000")
      .style("font-weight", this.config.axises?.y?.fontWeight || "bold")
      .style("dominant-baseline", "central");
  }

  private addGuideLines() {
    if (!this.config.guideLines) return;
    const guideLines = this.zoom
      .append("g")
      .classed("guidelines", true)
      .selectAll("g")
      .data(this.config.guideLines)
      .enter()
      .append("g")
      .classed("guideline", true);

    guideLines
      .append("path")
      .attr("d", (d: GuideLine) => {
        const path = d3.path();
        path.moveTo(this.startX, this.yscale(d.y));
        path.lineTo(this.xscale(d.x), this.yscale(d.y));
        path.lineTo(this.xscale(d.x), this.endY);
        return path.toString();
      })
      .style("stroke", (d) => d.color || "#000")
      .style("fill", "none")
      .style("stroke-width", (d) => d.width || 2)
      .style("stroke-dasharray", (d) => (d.type === "solid" ? "" : "6"));

    guideLines
      .append("text")
      .text((d) => (d.upLabel?.text ? d.upLabel.text : ""))
      .attr("x", (d) => this.xscale(d.x / 2))
      .attr("y", (d) => this.yscale(d.y + (d.upLabel?.offset || 30)))
      .style("dominant-baseline", "middle")
      .style("text-anchor", "middle")
      .style("font-size", (d) => d.fontSize || 16)
      .style("font-weight", (d) => d.fontWeight || "bold")
      .style("fill", (d) => d.upLabel?.color || d.color || "#000");

    guideLines
      .append("text")
      .text((d) => (d.downLabel?.text ? d.downLabel.text : ""))
      .attr("x", (d) => this.xscale(d.x / 2))
      .attr("y", (d) => this.yscale(d.y - (d.downLabel?.offset || 30)))
      .style("dominant-baseline", "middle")
      .style("text-anchor", "middle")
      .style("font-size", (d) => d.fontSize || 16)
      .style("font-weight", (d) => d.fontWeight || "bold")
      .style("fill", (d) => d.downLabel?.color || d.color || "#000");
  }

  private appendPoints() {
    const _this = this;
    const series = this.zoom.append("g").classed("series", true);
    series
      .selectAll("g")
      .data(this.config.series)
      .enter()
      .append("g")
      .classed("sery", true)
      .attr("sery", (d) => d.id + "")
      .selectAll("use")
      .data((d) => d.points)
      .enter()
      .append("use")
      .classed("point", true)
      .attr("width", (d) => Number(d.size) + 0)
      .attr("height", (d) => Number(d.size) + 0)
      .attr("xlink:href", function (this: SVGUseElement) {
        const sery = d3
          .select(this.parentNode as Element)
          .datum() as IndexChartSeryConfig;
        return "#" + sery.id;
      })
      .attr("x", (d) => _this.xscale(d.x) - Number(d.size) / 2)
      .attr("y", (d) => _this.yscale(d.y) - Number(d.size) / 2);
  }

  private appendLegends() {
    const legend = this.legends
      .selectAll("g")
      .data(this.config.series)
      .enter()
      .append("g")
      .classed("legend", true)
      .attr(`transform`, (d, i) => `translate(${this.w - 35}, ${i * 30 + 25})`);

    legend
      .append("text")
      .text((d) => d.label + "")
      .attr("x", 0)
      .attr("y", 0)
      .style("text-anchor", "end")
      .style("dominant-baseline", "middle");

    legend
      .append("use")
      .attr("width", 15)
      .attr("height", 15)
      .attr("x", 5)
      .attr("y", -7.5)
      .attr("xlink:href", (d) => "#" + d.id);
  }

  private bindEvents() {
    const _this = this;
    this.zoom
      .selectAll(".point")
      .on("mouseover", function (this, e: MouseEvent, d: any) {
        const pnode = d3.select((this as Element).parentNode as Element);
        const pdata = pnode.datum() as IndexChartSeryConfig;
        const data = d as IndexChartPoint;

        _this.tooltip.topic =
          pdata.label + (data.name ? ` -- ${data.name}` : "");
        if (_this.config.axises?.x?.label) {
          _this.tooltip.xlabel = _this.config.axises.x.label + " : " + data.x;
        } else {
          _this.tooltip.xlabel = "X轴 : " + data.x;
        }

        if (_this.config.axises?.y?.label) {
          _this.tooltip.ylabel = _this.config.axises.y.label + " : " + data.y;
        } else {
          _this.tooltip.ylabel = "y轴 : " + data.y;
        }

        _this.$ToolTip.style.left = e.clientX + 15 + "px";
        _this.$ToolTip.style.top = e.clientY + 15 + "px";

        _this.visible = true;
      })
      .on("mouseout", () => {
        this.visible = false;
      });
  }
}
</script>
<style lang="scss" scoped>
.index-chart {
  width: 100%;
  height: 100%;
  position: relative;
  main {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
  }
  section {
    position: fixed;
    z-index: 9999;
    left: 0;
    min-width: 150px;
    top: 0;
    background-color: rgba(0, 0, 0, 0.8);
    color: #fff;
    text-align: left;
    padding: 10px;
    border-radius: 6px;
    transition: all ease 0.3s;
    .topic {
      font-size: 14px;
      font-weight: bold;
      margin-bottom: 10px;
    }
    .label1 {
      font-size: 12px;
      & + .label1 {
        margin-top: 7px;
      }
    }
  }
}
</style>
